﻿#region Using Statements
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
using InputEventSystem;
using SkinnedModel;
#endregion

namespace XNAViewerSample
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class XNAViewer : Microsoft.Xna.Framework.Game
    {
        #region Fields
        GraphicsDeviceManager graphics;

        // Text UI
        private SpriteBatch spriteBatch;
        private SpriteFont spriteFont;
        private String message = "FPS: ";
        private float fps;
        private String debugMsg = "";
        private double accumElapsedTimeSec;
        private int frames;

        // Input system
        private InputEvents input;

        // Game Components
        private CameraComponent camera1;
        private ModelsComponent modelManager;
        private AnimationContainer animationContainer;
        private AnimationClip[] clip;
        private SkyboxComponent skybox1;

        // Game specific variables
        private float headRot, headArc;
        private float modelRot;
        private Vector3 modelMove;

        private const int windowWidth = 640;
        private const int windowHeight = 480;
        private Quaternion modelOrientation;
        private Vector3 modelPosition;

        #endregion

        public XNAViewer()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";

            graphics.PreferredBackBufferWidth = windowWidth;
            graphics.PreferredBackBufferHeight = windowHeight;
            graphics.PreferMultiSampling = true;

            //IsFixedTimeStep = false;      <-- need to be TRUE to sync properly with voice
            IsMouseVisible = true;
            this.Window.Title = "XNAViewer Demo";
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here
            // Add input handlers (keyboard and mouse)
            input = new InputEvents(this);
            this.Components.Add(input);
            input.KeyDown += new KeyDownHandler(KeyDownFunction);
            //input.MouseDown += new MouseDownHandler(MouseDownFunction);
            input.Enabled = true;

            // Add Camera component
            camera1 = new CameraComponent(this);
            this.Components.Add(camera1);
            //camera1.Position = new Vector3(0, 87, 400);
            //camera1.Target = new Vector3(0, 85, 0);
            //camera1.FoV = MathHelper.PiOver4 / 2;
            //camera1.Controls = input;
            //camera1.cameraHandler += new CameraHandler(cam1_cameraHandler);

            GraphicsDevice device = graphics.GraphicsDevice;
            float aspectRatio = (float)windowWidth / (float)windowHeight;
            camera1.Perspective(45.0f / 2, aspectRatio, 10.0f, 5000.0f);
            camera1.Position = new Vector3(0.0f, 127.0f, 400.0f);
            camera1.LookAt(new Vector3(0.0f, 125.0f, 0.0f));
            camera1.Acceleration = new Vector3(4.0f, 4.0f, 4.0f);
            camera1.Velocity = new Vector3(1.0f, 1.0f, 1.0f);
            camera1.OrbitMinZoom = 1.5f;
            camera1.OrbitMaxZoom = 5.0f;
            camera1.OrbitOffsetDistance = camera1.OrbitMinZoom;
            ChangeCameraBehavior(Camera.Behavior.Orbit);

            // Add Skybox component
            skybox1 = new SkyboxComponent(this);
            this.Components.Add(skybox1);
            skybox1.ActiveCamera = camera1;
            skybox1.Enabled = true;

            // Add Model Manager
            modelManager = new ModelsComponent(this);
            this.Components.Add(modelManager);
            modelManager.ActiveCamera = camera1;
            //modelManager.ActiveLightPosition = light1Pos;     <-- unused, since using stock shaders
            modelManager.Enabled = true;

            // Add animation container
            animationContainer = new AnimationContainer(this);
            this.Components.Add(animationContainer);
            animationContainer.animationHandler += new AnimationHandler(cont1_animationHandler);
            animationContainer.Enabled = true;

            // Game specific initialization
            headArc = 0.0f;
            headRot = 0.0f;
            modelMove = Vector3.Zero;
            modelRot = 0.0f;

            base.Initialize();
        }

        private void ChangeCameraBehavior(Camera.Behavior behavior)
        {
            if (camera1.CurrentBehavior == behavior)
                return;

            if (behavior == Camera.Behavior.Orbit)
            {
                modelPosition = camera1.Position;
                modelOrientation = Quaternion.Inverse(camera1.Orientation);
            }

            camera1.CurrentBehavior = behavior;

            // Position the camera behind and 30 degrees above the target.
            //if (behavior == Camera.Behavior.Orbit)
            //    camera1.Rotate(0.0f, -30.0f, 0.0f);
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);
            spriteFont = Content.Load<SpriteFont>("Fonts/Arial");

            // Load 3D game content <Models>
            modelManager.Add("YukiBody", "Models/Yuki/Yuki", ModelsComponent.ModelType.SKINNED);

            modelManager.Add("YukiHead", "Models/Yuki/Yuki-hd", ModelsComponent.ModelType.STATIC);
            modelManager.StaticModel["YukiHead"].Parent = "YukiBody";
            modelManager.StaticModel["YukiHead"].AttachTo = "head";

            // Customize models if necessary
            CMesh meshTag = new CMesh();
            meshTag = modelManager.StaticModel["YukiHead"].ModelInfo.Meshes[0].Tag as CMesh;
            meshTag.DrawOrder = -10;  // render head first

            // Get animation clips from skinningData
            SkinningData skinningData = modelManager.SkinModel["YukiBody"].TagInfo;
            clip = new AnimationClip[skinningData.AnimationClips.Count];
            clip[0] = skinningData.AnimationClips["Take 001"];

            // Create an animation player, and start decoding an animation clip.
            // NB: Required "Main" animation name
            modelManager.SkinModel["YukiBody"].Animation = animationContainer;
            modelManager.SkinModel["YukiBody"].Animation.Add("Main", skinningData, clip[0], AnimationPlayer.PlaybackMode.Forward);
            modelManager.SkinModel["YukiBody"].Animation.Play();
            //animationContainer.Add("Main", skinningData, clip[0], AnimationPlayer.PlaybackMode.LoopForward);
            // Create a disabled auxiliary player that plays idle one-time
            //animationContainer.Add("Aux", skinningData, clip[3], AnimationPlayer.PlaybackMode.Forward);
            //animationContainer.Players["Aux"].IsEnabled = false;
            //animationContainer.Play();            

            // Load the bounding spheres for all applicable skinned models
            modelManager.AddCollisionSphere(modelManager.SkinModel["YukiBody"], "Models/Yuki/CollisionSpheres");

        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            // procedural sample: let head follow camera movement
            // UpdateHeadMovement(gameTime, modelManager.SkinModel["YukiBody"]);

            base.Update(gameTime);
        }

        #region Callbacks and Handlers

#if false
        /// <summary>
        /// Game specific demo: Procedural Bone control
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values</param>
        /// <param name="mdl">Skinned model to update head movement</param>
        private void UpdateHeadMovement(GameTime gameTime, CSkinnedModel mdl)
        {
            // head moves accdg to camera:
            //  - rotation
            //  - arc
            //  - pan (atan(height/distance))
            //  - zoom (no change, zoom-in and zoom-out does not affect camera lookat)

            int headIndex = mdl.TagInfo.BoneIndices["head"];
            float delta = 0.5f;

            // TODO: ease out movement for more natural look
            // add hysteresis to prevent oscillation/bounce
            if (camera1.Rotation + modelRot - delta > headRot)
                headRot += MathHelper.Min(delta, (camera1.Rotation + modelRot -headRot)/2);
            else if (camera1.Rotation + modelRot + delta < headRot)
                headRot -= MathHelper.Min(delta, (headRot-camera1.Rotation - modelRot)/2);

            float camArc = MathHelper.ToDegrees((float)Math.Atan2(
                camera1.Height + (camera1.Position.Z + camera1.Distance) * Math.Tan((double)MathHelper.ToRadians(camera1.Arc)), 
                camera1.Position.Z + camera1.Distance));
            if (camArc - delta > headArc)
                headArc += MathHelper.Min(delta, (camArc - headArc) / 2);
            else if (camArc + delta < headArc)
                headArc -= MathHelper.Min(delta, (headArc - camArc) / 2);

            // limit head movement
            headRot = MathHelper.Clamp(headRot, -80.0f, +80.0f);
            headArc = MathHelper.Clamp(headArc, -30.0f, +30.0f);

            //debugMsg = camArc.ToString();

            mdl.Animation.Players["Main"].PlayerInfo.ProceduralTransforms[headIndex] =
                    Matrix.CreateRotationZ(MathHelper.ToRadians(-1 * headRot)) *
                    Matrix.CreateRotationX(MathHelper.ToRadians(-1 * headArc));
        }
#endif

        /// <summary>
        /// Event Handler for mouse button click
        /// </summary>
        /// <param name="args">Mouse event parameter</param>
        //private void MouseDownFunction(MouseEventArgs args)
        //{
        //}

        /// <summary>
        /// Event Handler for key press
        /// </summary>
        /// <param name="args">Key event parameter</param>
        private void KeyDownFunction(KeyEventArgs args, GameTime gameTime)
        {
            float time = (float)gameTime.ElapsedGameTime.TotalMilliseconds;

            // check for exit
            if (args.Key == Keys.Escape)
                Exit();

            // show bounding spheres
            if (args.Key == Keys.OemOpenBrackets)
                modelManager.showSpheres = !modelManager.showSpheres;

            // move model around
            Vector3 modelPos = Vector3.Zero;
            if (args.Key == Keys.Left)
                modelRot += time * 0.2f;
            if (args.Key == Keys.Right)
                modelRot -= time * 0.2f;
            modelPos.X = -(float)Math.Sin(MathHelper.ToRadians(modelRot));
            modelPos.Z = -(float)Math.Cos(MathHelper.ToRadians(modelRot));
            if (args.Key == Keys.Up)
                modelMove -= Vector3.Multiply(modelPos, 2.0f);
            if (args.Key == Keys.Down)
                modelMove += Vector3.Multiply(modelPos, 2.0f);
            modelManager.SkinModel["YukiBody"].Animation.modelTransform =
                Matrix.CreateRotationY(MathHelper.ToRadians(modelRot)) *
                Matrix.CreateTranslation(modelMove);
        }
#if false
        /// <summary>
        /// Handles camera update
        /// <param name="gameTime">Timing values snapshot</param>
        /// <param name="key">Key being pressed</param>
        /// </summary>
        private void cam1_cameraHandler(GameTime gameTime, Keys key)
        {
            //store current camera params
            float arc = camera1.Arc;
            float rotation = camera1.Rotation;
            float distance = camera1.Distance;
            float height = camera1.Height;
            float time = (float)gameTime.ElapsedGameTime.TotalMilliseconds;

            // Check for input to rotate the camera up and down around the model.
            if ((key == Keys.Up) || (key == Keys.W))
                camera1.Arc += time * 0.1f;
            if ((key == Keys.Down) || (key == Keys.S))
                camera1.Arc -= time * 0.1f;

            // Limit the arc movement.//TEMP
            //if (camera1.Arc > 40.0f)
            //    camera1.Arc = 40.0f;
            //else if (camera1.Arc < -30.0f)
            //    camera1.Arc = -30.0f;

            // Check for input to rotate the camera around the model.
            if ((key == Keys.Right) || (key == Keys.D))
                camera1.Rotation -= time * 0.1f;
            if ((key == Keys.Left) || (key == Keys.A))
                camera1.Rotation += time * 0.1f;

            // Check for input to zoom camera in and out.
            if (key == Keys.Q)
                camera1.Distance -= time * 0.2f;
            if (key == Keys.Z)
                camera1.Distance += time * 0.2f;

            // Limit the camera distance.//TEMP
            //if (camera1.Distance > 300.0f)
            //    camera1.Distance = 300.0f;
            //else if (camera1.Distance < -20.0f)
            //    camera1.Distance = -20.0f;

            // Check for input to pan camera up and down
            if (key == Keys.E)
                camera1.Height += time * 0.1f;
            if (key == Keys.C)
                camera1.Height -= time * 0.1f;

            // Limit the camera height.//TEMP

            // Reset to default camera
            if (key == Keys.X)
            {
                camera1.Arc = 0;
                camera1.Rotation = 0;
                camera1.Distance = 0;
                camera1.Height = 0;
            }

            // check camera collision here
            /*
            if (CheckCameraCollision(arc, distance))
            {
                camera1.Arc = arc;
                camera1.Distance = distance;
                camera1.Rotation = rotation;
                camera1.Height = height;
            }
             */
        }
#endif
        /// <summary>
        /// Event handler for animation status events
        /// </summary>
        /// <param name="playerName">ID of animation player</param>
        void cont1_animationHandler(string playerName, AnimationPlayer.PlaybackStatus status)
        {
            AnimationPlayer.PlaybackMode mode = animationContainer.Players[playerName].PlaybackMode;
#if false
            // TODO: debug: why always main-end?
            if (playerName == "Main")
            {
                if (status == AnimationPlayer.PlaybackStatus.End)
                {
                    //otherMsg += "\nmain-idle-forward";

                    if ((clipIndex != 0) && (animationContainer.Players["Aux"].PlaybackMode == AnimationPlayer.PlaybackMode.Forward))
                    {
                        // Main-idle-forward has finished, start an Aux-transition backward-forward
                        animationContainer.Players["Aux"].AnimationClips = clip[clipIndex];
                        animationContainer.Players["Aux"].PlaybackMode = AnimationPlayer.PlaybackMode.Backward;
                        animationContainer.Players["Aux"].FadeDuration = TimeSpan.FromSeconds(0.5f);
                        animationContainer.Players["Aux"].IsEnabled = true;
                        animationContainer.Play();
                    }
                    else if ((clipIndex != 0) && (animationContainer.Players["Aux"].PlaybackMode == AnimationPlayer.PlaybackMode.Backward))
                    {
                        // Main-transition-backward has finished, return to idle
                        animationContainer.Players["Main"].AnimationClips = clip[0];
                        animationContainer.Players["Main"].PlaybackMode = AnimationPlayer.PlaybackMode.LoopForward;
                        animationContainer.Players["Main"].FadeDuration = TimeSpan.FromSeconds(0.5f);
                        animationContainer.Players["Aux"].IsEnabled = false;
                        animationContainer.Play();

                        // reset clip and idle timer
                        clipIndex = 0;
                        idleTimer = TimeSpan.Zero;
                    }
                }
            }
            if (playerName == "Aux")
            {
                if (status == AnimationPlayer.PlaybackStatus.End)
                {
                    if (mode == AnimationPlayer.PlaybackMode.Forward)
                    {
                        //otherMsg += "\naux-transition-forward";
                    }
                    else if (mode == AnimationPlayer.PlaybackMode.Backward)
                    {
                        //otherMsg += "\naux-transition-backward";
                        animationContainer.Players["Aux"].IsEnabled = false;
                    }
                }
            }
#endif
        }

        #endregion

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            //GraphicsDevice.Clear(Color.CornflowerBlue);
            GraphicsDevice.Clear(Color.Black);

            //graphics.GraphicsDevice.RenderState.DepthBufferEnable = true;
            GraphicsDevice.BlendState = BlendState.AlphaBlend;
            GraphicsDevice.DepthStencilState = DepthStencilState.Default;
            GraphicsDevice.SamplerStates[0] = SamplerState.LinearWrap;

            RasterizerState rasterizerState1 = new RasterizerState();
            rasterizerState1.CullMode = CullMode.CullCounterClockwiseFace;
            rasterizerState1.MultiSampleAntiAlias = true;
            GraphicsDevice.RasterizerState = rasterizerState1;
            GraphicsDevice.PresentationParameters.MultiSampleCount = 2;

            base.Draw(gameTime);

            //fps = 1000.0f / (float)gameTime.ElapsedGameTime.Milliseconds;
            accumElapsedTimeSec += (float)gameTime.ElapsedGameTime.TotalSeconds;
            if (accumElapsedTimeSec >= 1.0f)
            {
                fps = frames;
                frames = 0;
                accumElapsedTimeSec = 0.0;

            }
            else
            {
                frames++;
            }
            spriteBatch.Begin();
            spriteBatch.DrawString(spriteFont, message + fps.ToString() + "\n" + debugMsg,
                new Vector2(0.0f, 0.0f), Color.Yellow);
            spriteBatch.End();
        }

    }
}
